package com.swapper.json.reflect;

import com.swapper.json.config.MemberNamingPolicy;
import com.swapper.json.config.MemberNamingStrategy;
import com.swapper.json.JsonWrapper;
import com.swapper.json.annotations.*;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.TimeZone;

public final class FieldAttributes {
  private final Field _field;
  private final JsonWrapper<Object> _wrapper;
  private final String _name;
  private final boolean _excludeSerialize;
  private final boolean _excludeDeserialize;

  public FieldAttributes(Field field, JsonWrapper<Object> wrapper) {
    _field = field;
    _field.setAccessible(true);
    _wrapper = wrapper;
    _name = findFieldNamingStrategy(field).translate(field);
    FieldExclude exclude = field.getAnnotation(FieldExclude.class);
    _excludeSerialize = exclude != null && exclude.serialize();
    _excludeDeserialize = exclude != null && exclude.deserialize();
  }

  public String nameOfMember() {
    return _name;
  }

  public boolean is_excludeSerialize() {
    return _excludeSerialize;
  }

  public boolean is_excludeDeserialize() {
    return _excludeDeserialize;
  }

  public Object get(Object instance) throws IllegalAccessException {
    return _field.get(instance);
  }

  public void set(Object instance, Object value) throws IllegalAccessException {
    _field.set(instance, value);
  }

  public JsonWrapper<Object> getWrapper() {
    return _wrapper;
  }

  private static MemberNamingStrategy findFieldNamingStrategy(Field field) {
    FieldName fieldName;
    if ((fieldName = field.getAnnotation(FieldName.class)) != null) {
      return f -> fieldName.value();
    }
    FieldNamingStrategySource strategySource;
    if ((strategySource = field.getAnnotation(FieldNamingStrategySource.class)) != null) {
      try {
        return strategySource.value().getDeclaredConstructor()
          .newInstance().provideStrategy();
      } catch (InstantiationException | IllegalAccessException
        | InvocationTargetException | NoSuchMethodException e) {
        // do nothing
      }
    }
    FieldNamingPolicySource policySource;
    if ((policySource = field.getAnnotation(FieldNamingPolicySource.class)) != null) {
      return policySource.value();
    }
    Class<?> declaringClass = field.getDeclaringClass();
    if ((strategySource = declaringClass.getAnnotation(FieldNamingStrategySource.class)) != null) {
      try {
        return strategySource.value().getDeclaredConstructor()
          .newInstance().provideStrategy();
      } catch (InstantiationException | IllegalAccessException
        | InvocationTargetException | NoSuchMethodException e) {
        // do nothing
      }
    }
    if ((policySource = declaringClass.getAnnotation(FieldNamingPolicySource.class)) != null) {
      return policySource.value();
    }
    return MemberNamingPolicy.DEFAULT;
  }

  public static DateFormat findDateFormat(Field field) {
    DateTimeFormat format;
    if ((format = field.getAnnotation(DateTimeFormat.class)) != null) {
      String pattern = format.pattern();
      if (pattern == null || pattern.isEmpty()) {
        throw new IllegalArgumentException("DateFormat requires a pattern.");
      }
      Locale locale = parseLocale(format.locale());
      TimeZone timeZone = parseTimeZone(format.timezone());
      DateFormat formatter = new SimpleDateFormat(pattern, locale);
      formatter.setTimeZone(timeZone);
      return formatter;
    }
    return null;
  }

  public static DateTimeFormatter findDateTimeFormatter(Field field) {
    DateTimeFormat format;
    if ((format = field.getAnnotation(DateTimeFormat.class)) != null) {
      String pattern = format.pattern();
      if (pattern == null || pattern.isEmpty()) {
        throw new IllegalArgumentException("DateTimeFormatter requires a pattern.");
      }
      Locale locale = parseLocale(format.locale());
      ZoneId timeZone = parseZoneId(format.timezone());
      return DateTimeFormatter.ofPattern(pattern, locale).withZone(timeZone);
    }
    return null;
  }

  private static Locale parseLocale(String locale) {
    if (locale == null || locale.isEmpty()) {
      return Locale.getDefault();
    }
    StringTokenizer tokenizer = new StringTokenizer(locale, "_");
    String language = tokenizer.hasMoreElements() ? tokenizer.nextToken() : "";
    if (language.isEmpty()) {
      throw new IllegalArgumentException("Locale requires a language.");
    }
    String country = tokenizer.hasMoreElements() ? tokenizer.nextToken() : "";
    String variant = tokenizer.hasMoreElements() ? tokenizer.nextToken() : "";
    return new Locale(language, country, variant);
  }

  private static TimeZone parseTimeZone(String timezone) {
    return timezone == null || timezone.isEmpty()
      ? TimeZone.getDefault()
      : TimeZone.getTimeZone(timezone);
  }

  private static ZoneId parseZoneId(String timezone) {
    return timezone == null || timezone.isEmpty()
      ? ZoneId.systemDefault()
      : ZoneId.of(timezone);
  }
}
